package com.lemon.cloud.redis.lock;

import com.lemon.cloud.lock.AbstractDistributedLock;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.util.Assert;

import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * @author lemon
 * @version 1.0
 * @description: Redis实现分布式锁
 * @date Create by lemon on 2019-09-05 12:41
 */
@Slf4j
public class RedisDistributedLock extends AbstractDistributedLock {
    @Value("${redis.distributed-lock.unlock.lua-script:if redis.call(\"get\",KEYS[1]) == ARGV[1] \r\n then \r\n return redis.call(\"del\",KEYS[1]) \r\n else \r\n return 0 \r\n end}")
    private String unlockLuaScript;

    @Setter
    @Getter
    private StringRedisTemplate redisTemplate;

    /**
     * 当前获取分布式锁的线程持有的key
     */
    private static final ThreadLocal<String> CURRENT_THREAD_LOCK_VALUE = new ThreadLocal<>();

    /**
     * @param key
     * @param expireTime
     * @param expireTimeUnit
     * @return boolean
     * @description
     * @author lemon
     * @date 2019-09-08 20:13
     */
    @Override
    protected boolean acquire(final String key, long expireTime, TimeUnit expireTimeUnit) {
        try {
            final String uuid = UUID.randomUUID().toString();
            CURRENT_THREAD_LOCK_VALUE.set(uuid);

            boolean status = this.redisTemplate.execute((RedisCallback<Boolean>) connection -> {
                byte[] keyByte = this.redisTemplate.getStringSerializer().serialize(key);
                byte[] uuidByte = this.redisTemplate.getStringSerializer().serialize(uuid);
                boolean result = connection.set(keyByte, uuidByte, Expiration.from(expireTime, expireTimeUnit), RedisStringCommands.SetOption.ifAbsent());
                return result;
            });

            return status;
        } catch (Exception e) {
            log.error("Obtaining an exception for the redis distributed lock", e);
        }

        return false;
    }

    /**
     * @param key key值
     * @return void
     * @description 释放锁
     * @author lemon
     * @date 2019-09-04 12:49
     */
    @Override
    public boolean unlock(String key) {
        // 释放锁的时候，有可能因为持锁之后方法执行时间大于锁的有效期，此时有可能已经被另外一个线程持有锁，所以不能直接删除
        try {
            // 使用lua脚本删除redis中匹配value的key，可以避免由于方法执行时间过长而redis锁自动过期失效的时候误删其他线程的锁
            // Spring自带的执行脚本方法中，集群模式直接抛出不支持执行脚本的异常，所以只能拿到原redis的connection来执行脚本
            Boolean result = this.redisTemplate.execute((RedisCallback<Boolean>) connection -> {
                byte[] scriptByte = this.redisTemplate.getStringSerializer().serialize(this.unlockLuaScript);

                return connection.eval(scriptByte, ReturnType.BOOLEAN, 1,
                        RedisDistributedLock.this.redisTemplate.getStringSerializer().serialize(key),
                        RedisDistributedLock.this.redisTemplate.getStringSerializer().serialize(CURRENT_THREAD_LOCK_VALUE.get()));
            });

            return result;
        } catch (Exception e) {
            log.error("Release redis distributeLock occured an exception", e);
        } finally {
            CURRENT_THREAD_LOCK_VALUE.remove();
        }

        return false;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        Assert.state(this.getRedisTemplate() != null, "StringRedisTemplate is required");
    }
}
